/*
 * Copyright 2012 Hongdian, Inc. All rights reserved.
 */
package us.utils.crypto;

import java.io.*;

import us.utils.exception.CodecException;


/**
 * Base abstract class that provides useful encoding and decoding operations,
 * especially for character data.
 * 
 * @since 0.9
 */
public abstract class CodecSupport {

	/**
	 * Shiro's default preferred character encoding, equal to <b>
	 * <code>UTF-8</code></b>.
	 */
	public static final String PREFERRED_ENCODING = "UTF-8";

	/**
	 * Converts the specified character array to a byte array using the Shiro's
	 * preferred encoding (UTF-8).
	 * <p/>
	 * This is a convenience method equivalent to calling the
	 * {@link #toBytes(String,String)} method with a a wrapping String and
	 * {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}, i.e.
	 * <p/>
	 * <code>toBytes( new String(chars), {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING} );</code>
	 * 
	 * @param chars
	 *            the character array to be converted to a byte array.
	 * @return the byte array of the UTF-8 encoded character array.
	 */
	public static byte[] toBytes(char[] chars) {
		return toBytes(new String(chars), PREFERRED_ENCODING);
	}

	/**
	 * Converts the specified character array into a byte array using the
	 * specified character encoding.
	 * <p/>
	 * This is a convenience method equivalent to calling the
	 * {@link #toBytes(String,String)} method with a a wrapping String and the
	 * specified encoding, i.e.
	 * <p/>
	 * <code>toBytes( new String(chars), encoding );</code>
	 * 
	 * @param chars
	 *            the character array to be converted to a byte array
	 * @param encoding
	 *            the character encoding to use to when converting to bytes.
	 * @return the bytes of the specified character array under the specified
	 *         encoding.
	 * @throws CodecException
	 *             if the JVM does not support the specified encoding.
	 */
	public static byte[] toBytes(char[] chars, String encoding) throws CodecException {
		return toBytes(new String(chars), encoding);
	}

	/**
	 * Converts the specified source argument to a byte array with Shiro's
	 * {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}.
	 * 
	 * @param source
	 *            the string to convert to a byte array.
	 * @return the bytes representing the specified string under the
	 *         {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}.
	 * @see #toBytes(String, String)
	 */
	public static byte[] toBytes(String source) {
		return toBytes(source, PREFERRED_ENCODING);
	}

	/**
	 * Converts the specified source to a byte array via the specified encoding,
	 * throwing a {@link CodecException CodecException} if the encoding fails.
	 * 
	 * @param source
	 *            the source string to convert to a byte array.
	 * @param encoding
	 *            the encoding to use to use.
	 * @return the byte array of the specified source with the given encoding.
	 * @throws CodecException
	 *             if the JVM does not support the specified encoding.
	 */
	public static byte[] toBytes(String source, String encoding) throws CodecException {
		try {
			return source.getBytes(encoding);
		} catch (UnsupportedEncodingException e) {
			String msg = "Unable to convert source [" + source + "] to byte array using " + "encoding '" + encoding + "'";
			throw new CodecException(msg, e);
		}
	}

	/**
	 * Converts the specified byte array to a String using the
	 * {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}.
	 * 
	 * @param bytes
	 *            the byte array to turn into a String.
	 * @return the specified byte array as an encoded String (
	 *         {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}).
	 * @see #toString(byte[], String)
	 */
	public static String toString(byte[] bytes) {
		return toString(bytes, PREFERRED_ENCODING);
	}

	/**
	 * Converts the specified byte array to a String using the specified
	 * character encoding. This implementation does the same thing as
	 * <code>new {@link String#String(byte[], String) String(byte[], encoding)}</code>
	 * , but will wrap any {@link UnsupportedEncodingException} with a nicer
	 * runtime {@link CodecException}, allowing you to decide whether or not you
	 * want to catch the exception or let it propagate.
	 * 
	 * @param bytes
	 *            the byte array to convert to a String
	 * @param encoding
	 *            the character encoding used to encode the String.
	 * @return the specified byte array as an encoded String
	 * @throws CodecException
	 *             if the JVM does not support the specified encoding.
	 */
	public static String toString(byte[] bytes, String encoding) throws CodecException {
		try {
			return new String(bytes, encoding);
		} catch (UnsupportedEncodingException e) {
			String msg = "Unable to convert byte array to String with encoding '" + encoding + "'.";
			throw new CodecException(msg, e);
		}
	}

	/**
	 * Returns the specified byte array as a character array using the
	 * {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}.
	 * 
	 * @param bytes
	 *            the byte array to convert to a char array
	 * @return the specified byte array encoded as a character array (
	 *         {@link CodecSupport#PREFERRED_ENCODING PREFERRED_ENCODING}).
	 * @see #toChars(byte[], String)
	 */
	public static char[] toChars(byte[] bytes) {
		return toChars(bytes, PREFERRED_ENCODING);
	}

	/**
	 * Converts the specified byte array to a character array using the
	 * specified character encoding.
	 * <p/>
	 * Effectively calls
	 * <code>{@link #toString(byte[], String) toString(bytes,encoding)}.{@link String#toCharArray() toCharArray()};</code>
	 * 
	 * @param bytes
	 *            the byte array to convert to a String
	 * @param encoding
	 *            the character encoding used to encode the bytes.
	 * @return the specified byte array as an encoded char array
	 * @throws CodecException
	 *             if the JVM does not support the specified encoding.
	 */
	public static char[] toChars(byte[] bytes, String encoding) throws CodecException {
		return toString(bytes, encoding).toCharArray();
	}

	/**
	 * Returns {@code true} if the specified object can be easily converted to
	 * bytes by instances of this class, {@code false} otherwise.
	 * <p/>
	 * The default implementation returns {@code true} IFF the specified object
	 * is an instance of one of the following types:
	 * <ul>
	 * <li>{@code byte[]}</li>
	 * <li>{@code char[]}</li>
	 * <li>{@link ByteSource}</li>
	 * <li>{@link String}</li>
	 * <li>{@link File}</li>
	 * </li>{@link InputStream}</li>
	 * </ul>
	 * 
	 * @param o
	 *            the object to test to see if it can be easily converted to a
	 *            byte array
	 * @return {@code true} if the specified object can be easily converted to
	 *         bytes by instances of this class, {@code false} otherwise.
	 * @since 1.0
	 */
	protected boolean isByteSource(Object o) {
		return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File
				|| o instanceof InputStream;
	}

	/**
	 * Converts the specified Object into a byte array.
	 * <p/>
	 * If the argument is a {@code byte[]}, {@code char[]}, {@link ByteSource},
	 * {@link String}, {@link File}, or {@link InputStream}, it will be
	 * converted automatically and returned.}
	 * <p/>
	 * If the argument is anything other than these types, it is passed to the
	 * {@link #objectToBytes(Object) objectToBytes} method which must be
	 * overridden by subclasses.
	 * 
	 * @param o
	 *            the Object to convert into a byte array
	 * @return a byte array representation of the Object argument.
	 */
	protected byte[] toBytes(Object o) {
		if (o == null) {
			String msg = "Argument for byte conversion cannot be null.";
			throw new IllegalArgumentException(msg);
		}
		if (o instanceof byte[]) {
			return (byte[]) o;
		} else if (o instanceof ByteSource) {
			return ((ByteSource) o).getBytes();
		} else if (o instanceof char[]) {
			return toBytes((char[]) o);
		} else if (o instanceof String) {
			return toBytes((String) o);
		} else if (o instanceof File) {
			return toBytes((File) o);
		} else if (o instanceof InputStream) {
			return toBytes((InputStream) o);
		} else {
			return objectToBytes(o);
		}
	}

	/**
	 * Converts the specified Object into a String.
	 * <p/>
	 * If the argument is a {@code byte[]} or {@code char[]} it will be
	 * converted to a String using the {@link #PREFERRED_ENCODING}. If a String,
	 * it will be returned as is.
	 * <p/>
	 * If the argument is anything other than these three types, it is passed to
	 * the {@link #objectToString(Object) objectToString} method.
	 * 
	 * @param o
	 *            the Object to convert into a byte array
	 * @return a byte array representation of the Object argument.
	 */
	protected String toString(Object o) {
		if (o == null) {
			String msg = "Argument for String conversion cannot be null.";
			throw new IllegalArgumentException(msg);
		}
		if (o instanceof byte[]) {
			return toString((byte[]) o);
		} else if (o instanceof char[]) {
			return new String((char[]) o);
		} else if (o instanceof String) {
			return (String) o;
		} else {
			return objectToString(o);
		}
	}

	protected byte[] toBytes(File file) {
		if (file == null) {
			throw new IllegalArgumentException("File argument cannot be null.");
		}
		try {
			return toBytes(new FileInputStream(file));
		} catch (FileNotFoundException e) {
			String msg = "Unable to acquire InputStream for file [" + file + "]";
			throw new CodecException(msg, e);
		}
	}

	/**
	 * Converts the specified {@link InputStream InputStream} into a byte array.
	 * 
	 * @param in
	 *            the InputStream to convert to a byte array
	 * @return the bytes of the input stream
	 * @throws IllegalArgumentException
	 *             if the {@code InputStream} argument is {@code null}.
	 * @throws CodecException
	 *             if there is any problem reading from the {@link InputStream}.
	 * @since 1.0
	 */
	protected byte[] toBytes(InputStream in) {
		if (in == null) {
			throw new IllegalArgumentException("InputStream argument cannot be null.");
		}
		final int BUFFER_SIZE = 512;
		ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE);
		byte[] buffer = new byte[BUFFER_SIZE];
		int bytesRead;
		try {
			while ((bytesRead = in.read(buffer)) != -1) {
				out.write(buffer, 0, bytesRead);
			}
			return out.toByteArray();
		} catch (IOException ioe) {
			throw new CodecException(ioe);
		} finally {
			try {
				in.close();
			} catch (IOException ignored) {
			}
			try {
				out.close();
			} catch (IOException ignored) {
			}
		}
	}

	/**
	 * Default implementation throws a CodecException immediately since it can't
	 * infer how to convert the Object to a byte array. This method must be
	 * overridden by subclasses if anything other than the three default types
	 * (listed in the {@link #toBytes(Object) toBytes(Object)} JavaDoc) are to
	 * be converted to a byte array.
	 * 
	 * @param o
	 *            the Object to convert to a byte array.
	 * @return a byte array representation of the Object argument.
	 */
	protected byte[] objectToBytes(Object o) {
		String msg = "The " + getClass().getName() + " implementation only supports conversion to "
				+ "byte[] if the source is of type byte[], char[], String, " + ByteSource.class.getName()
				+ " File or InputStream.  The instance provided as a method " + "argument is of type [" + o.getClass().getName()
				+ "].  If you would like to convert "
				+ "this argument type to a byte[], you can 1) convert the argument to one of the supported types "
				+ "yourself and then use that as the method argument or 2) subclass " + getClass().getName()
				+ "and override the objectToBytes(Object o) method.";
		throw new CodecException(msg);
	}

	/**
	 * Default implementation merely returns
	 * <code>objectArgument.toString()</code>. Subclasses can override this
	 * method for different mechanisms of converting an object to a String.
	 * 
	 * @param o
	 *            the Object to convert to a byte array.
	 * @return a String representation of the Object argument.
	 */
	protected String objectToString(Object o) {
		return o.toString();
	}
}
